package org.micropayment.provider.oauth.service;

import org.micropayment.model.MicropaymentUser;
import org.micropayment.model.Token;
import org.micropayment.model.service.MicropaymentConsumerDAO;
import org.micropayment.model.service.TokenDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.security.*;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.commons.codec.binary.Base64;
import java.util.*;
import java.security.SecureRandom;
import org.springframework.security.oauth.provider.token.ExpiredOAuthTokenException;
import org.springframework.security.oauth.provider.token.InvalidOAuthTokenException;
import org.springframework.security.oauth.provider.token.OAuthAccessProviderToken;
import org.springframework.security.oauth.provider.token.OAuthProviderToken;
import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices;
import org.springframework.security.oauth.provider.token.OAuthTokenLifecycleListener;
import org.springframework.security.oauth.provider.token.OAuthTokenLifecycleRegistry;
import org.springframework.context.annotation.Scope;

@Scope("prototype")
@Service("tokenServices")
public class MicropaymentProviderTokenServices implements
        OAuthProviderTokenServices, InitializingBean, OAuthTokenLifecycleRegistry {

    private static Logger logger =
            LoggerFactory.getLogger(MicropaymentProviderTokenServices.class);

    @Autowired TokenDAO tokenDao;
    @Autowired MicropaymentConsumerDAO consumerDao;

    private Random random;
    private int requestTokenValiditySeconds = 60 * 10; //default 10 minutes.
    private int accessTokenValiditySeconds = 60 * 60 * 12; //default 12 hours.
    private int tokenSecretLengthBytes = 80;
    private final Collection<OAuthTokenLifecycleListener> lifecycleListeners =
            new HashSet<OAuthTokenLifecycleListener>();


    public OAuthProviderTokenImpl readToken(String value) {
        Token token = getTokenDao().findById(value);
        return getOAuthProviderTokenImpl(token);
    }

    public OAuthProviderTokenImpl removeToken(String string) {
        Token token = getTokenDao().findById(string);
        token.setRemoved(Boolean.TRUE);
        return getOAuthProviderTokenImpl(getTokenDao().update(token));
    }

    protected OAuthProviderTokenImpl getOAuthProviderTokenImpl (Token token) {
        OAuthProviderTokenImpl providerTokenImpl =
                new OAuthProviderTokenImpl ();
        providerTokenImpl.setAccessToken(token.getIsAccessToken());
        providerTokenImpl.setCallbackUrl(token.getCallbackUrl());
        providerTokenImpl.setConsumerKey(
                token.getMicropaymentConsumerId().getConsumerKey());
        providerTokenImpl.setSecret(token.getSecret());
        MicropaymentUser user = token.getMicropaymentUserId();
        if (user != null) {
            UsernamePasswordAuthenticationToken usernamePasswordAuthentication =
            new UsernamePasswordAuthenticationToken(
                token.getMicropaymentUserId(), token.getMicropaymentUserId().getPassword());
            providerTokenImpl.setUserAuthentication(usernamePasswordAuthentication);
        }
        if (token.getVerifier() != null && !token.getIsAccessToken()) {
            providerTokenImpl.setVerifier(token.getVerifier());
            providerTokenImpl.setAccessToken(Boolean.FALSE);
            providerTokenImpl.setTimestamp(token.getRequestAuthorizationDate().getTime());
        } else if (token.getIsAccessToken()) {
            providerTokenImpl.setAccessToken(Boolean.TRUE);
            providerTokenImpl.setTimestamp(token.getAccessConfirmationDate().getTime());
        } else if (token.getVerifier() == null && !token.getIsAccessToken()) {
            providerTokenImpl.setTimestamp(token.getRequestUnauthorizedDate().getTime());
        }
        providerTokenImpl.setValue(token.getTokenValue());
        return providerTokenImpl;
    }

    protected Token getToken (String string, OAuthProviderTokenImpl oapti) {
        Token token = null;
        if (oapti.getVerifier() != null && !oapti.isAccessToken()) {
            token = getTokenDao().findById(oapti.getValue());
            token.setVerifier(oapti.getVerifier());
            token.setIsAccessToken(Boolean.FALSE);
            token.setRequestAuthorizationDate(new Date(oapti.getTimestamp()));
        } else if (oapti.isAccessToken()) {
            token = new Token();
            token.setIsAccessToken(oapti.isAccessToken());
            token.setAccessConfirmationDate(new Date(oapti.getTimestamp()));
        } else if (oapti.getVerifier() == null && !oapti.isAccessToken()) {
            token = new Token();
            token.setIsAccessToken(Boolean.FALSE);
            token.setRequestUnauthorizedDate(new Date(oapti.getTimestamp()));
        }
        token.setCallbackUrl(oapti.getCallbackUrl());
        token.setMicropaymentConsumerId(
                getConsumerDao().findByConsumerKey(oapti.getConsumerKey()));
        Authentication userAuthentication = oapti.getUserAuthentication();
        if (userAuthentication != null) {
            token.setMicropaymentUserId(
                (MicropaymentUser)userAuthentication.getPrincipal());
        }
        token.setSecret(oapti.getSecret());
        token.setTokenValue(oapti.getValue());
        return token;
    }

  /**
   * Initialze these token services. If no random generator is set, one will be created.
   *
   * @throws Exception
   */
  public void afterPropertiesSet() throws Exception {
    if (random == null) {
      random = new SecureRandom();
    }
  }

  public OAuthProviderToken getToken(String token) throws AuthenticationException {
    OAuthProviderTokenImpl tokenImpl = readToken(token);

    if (tokenImpl == null) {
      throw new InvalidOAuthTokenException("Invalid token: " + token);
    }
    else if (isExpired(tokenImpl)) {
      removeToken(token);
      onTokenRemoved(tokenImpl);
      throw new ExpiredOAuthTokenException("Expired token.");
    }

    return tokenImpl;
  }

  /**
   * Whether the auth token is expired.
   *
   * @param authToken The auth token to check for expiration.
   * @return Whether the auth token is expired.
   */
  protected boolean isExpired(OAuthProviderTokenImpl authToken) {
    if (authToken.isAccessToken()) {
      if ((authToken.getTimestamp() + (getAccessTokenValiditySeconds() * 1000)) < System.currentTimeMillis()) {
        return true;
      }
    }
    else {
      if ((authToken.getTimestamp() + (getRequestTokenValiditySeconds() * 1000)) < System.currentTimeMillis()) {
        return true;
      }
    }

    return false;
  }

  public OAuthProviderToken createUnauthorizedRequestToken(String consumerKey, String callbackUrl) throws AuthenticationException {
    String tokenValue = UUID.randomUUID().toString();
    byte[] secretBytes = new byte[getTokenSecretLengthBytes()];
    getRandom().nextBytes(secretBytes);
    String secret = new String(Base64.encodeBase64(secretBytes));
    OAuthProviderTokenImpl token = new OAuthProviderTokenImpl();
    token.setAccessToken(false);
    token.setConsumerKey(consumerKey);
    token.setCallbackUrl(callbackUrl);
    token.setUserAuthentication(null);
    token.setSecret(secret);
    token.setValue(tokenValue);
    token.setTimestamp(System.currentTimeMillis());
    onTokenCreated(token);
    getTokenDao().save(getToken(tokenValue, token));
    return token;
  }

  public void authorizeRequestToken(String requestToken, String verifier, Authentication authentication) throws AuthenticationException {
    OAuthProviderTokenImpl tokenImpl = readToken(requestToken);
    if (tokenImpl == null) {
      throw new InvalidOAuthTokenException("Invalid token: " + requestToken);
    }
    else if (isExpired(tokenImpl)) {
      removeToken(requestToken);
      onTokenRemoved(tokenImpl);
      throw new ExpiredOAuthTokenException("Expired token.");
    }
    else if (tokenImpl.isAccessToken()) {
      throw new InvalidOAuthTokenException("Request to authorize an access token.");
    }
    tokenImpl.setUserAuthentication(authentication);
    tokenImpl.setTimestamp(System.currentTimeMillis());//reset the expiration.
    tokenImpl.setVerifier(verifier);
    getTokenDao().update(getToken(requestToken, tokenImpl));
  }

  public OAuthAccessProviderToken createAccessToken(String requestToken) throws AuthenticationException {
    OAuthProviderTokenImpl tokenImpl = readToken(requestToken);
    if (tokenImpl == null) {
      throw new InvalidOAuthTokenException("Invalid token: " + requestToken);
    }
    else if (isExpired(tokenImpl)) {
      removeToken(requestToken);
      onTokenRemoved(tokenImpl);
      throw new ExpiredOAuthTokenException("Expired token.");
    }
    else if (tokenImpl.isAccessToken()) {
      throw new InvalidOAuthTokenException("Not a request token.");
    }
    else if (tokenImpl.getUserAuthentication() == null) {
      throw new InvalidOAuthTokenException("Request token has not been authorized.");
    }

    OAuthProviderTokenImpl requestTokenImpl = removeToken(requestToken);
    if (requestTokenImpl != null) {
      onTokenRemoved(requestTokenImpl);
    }

    String tokenValue = UUID.randomUUID().toString();
    byte[] secretBytes = new byte[getTokenSecretLengthBytes()];
    getRandom().nextBytes(secretBytes);
    String secret = new String(Base64.encodeBase64(secretBytes));
    OAuthProviderTokenImpl token = new OAuthProviderTokenImpl();
    token.setAccessToken(true);
    token.setConsumerKey(tokenImpl.getConsumerKey());
    token.setUserAuthentication(tokenImpl.getUserAuthentication());
    token.setSecret(secret);
    token.setValue(tokenValue);
    token.setTimestamp(System.currentTimeMillis());
    onTokenCreated(token);
    Token micropaymentToken = getToken(tokenValue, token);
    micropaymentToken.setRequestTokenValue(requestToken);
    getTokenDao().save(micropaymentToken);
    return token;
  }

  /**
   * Logic for handling event firing of a removed token.
   *
   * @param token The token that was removed (possibly null).
   */
  protected void onTokenRemoved(OAuthProviderTokenImpl token) {
    for (OAuthTokenLifecycleListener listener : getLifecycleListeners()) {
      listener.tokenExpired(token);
    }
  }

  /**
   * Logic for handling event firing of a created token.
   *
   * @param token The token that was created.
   */
  protected void onTokenCreated(OAuthProviderTokenImpl token) {
    for (OAuthTokenLifecycleListener listener : getLifecycleListeners()) {
      listener.tokenCreated(token);
    }
  }

  /**
   * The length of the token secret in bytes, before being base64-encoded.
   *
   * @return The length of the token secret in bytes.
   */
  public int getTokenSecretLengthBytes() {
    return tokenSecretLengthBytes;
  }

  /**
   * The length of the token secret in bytes, before being base64-encoded.
   *
   * @param tokenSecretLengthBytes The length of the token secret in bytes, before being base64-encoded.
   */
  public void setTokenSecretLengthBytes(int tokenSecretLengthBytes) {
    this.tokenSecretLengthBytes = tokenSecretLengthBytes;
  }

  /**
   * The random value generator used to create token secrets.
   *
   * @return The random value generator used to create token secrets.
   */
  public Random getRandom() {
    return random;
  }

  /**
   * The random value generator used to create token secrets.
   *
   * @param random The random value generator used to create token secrets.
   */
  public void setRandom(Random random) {
    this.random = random;
  }

  /**
   * The validity (in seconds) of the unauthenticated request token.
   *
   * @return The validity (in seconds) of the unauthenticated request token.
   */
  public int getRequestTokenValiditySeconds() {
    return requestTokenValiditySeconds;
  }

  /**
   * The validity (in seconds) of the unauthenticated request token.
   *
   * @param requestTokenValiditySeconds The validity (in seconds) of the unauthenticated request token.
   */
  public void setRequestTokenValiditySeconds(int requestTokenValiditySeconds) {
    this.requestTokenValiditySeconds = requestTokenValiditySeconds;
  }

  /**
   * The validity (in seconds) of the access token.
   *
   * @return The validity (in seconds) of the access token.
   */
  public int getAccessTokenValiditySeconds() {
    return accessTokenValiditySeconds;
  }

  /**
   * The validity (in seconds) of the access token.
   *
   * @param accessTokenValiditySeconds The validity (in seconds) of the access token.
   */
  public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {
    this.accessTokenValiditySeconds = accessTokenValiditySeconds;
  }

  /**
   * The collection of lifecycle listeners for these services.
   *
   * @return The collection of lifecycle listeners for these services.
   */
  public Collection<OAuthTokenLifecycleListener> getLifecycleListeners() {
    return lifecycleListeners;
  }

  /**
   * Register lifecycle listener(s) with these token services.
   *
   * @param lifecycleListeners The listeners.
   */
  @Autowired ( required = false )
  public void register(OAuthTokenLifecycleListener... lifecycleListeners) {
    if (lifecycleListeners != null) {
      this.lifecycleListeners.addAll(Arrays.asList(lifecycleListeners));
    }
  }

    /**
     * @return the tokenDao
     */
    public TokenDAO getTokenDao() {
        return tokenDao;
    }

    /**
     * @param tokenDao the tokenDao to set
     */
    public void setTokenDao(TokenDAO tokenDao) {
        this.tokenDao = tokenDao;
    }

    /**
     * @return the consumerDao
     */
    public MicropaymentConsumerDAO getConsumerDao() {
        return consumerDao;
    }

    /**
     * @param consumerDao the consumerDao to set
     */
    public void setConsumerDao(MicropaymentConsumerDAO consumerDao) {
        this.consumerDao = consumerDao;
    }
}
